0.3 把数据包送到了正确的机器。可一台机器上同时跑着浏览器、SSH、数据库、十几个后台服务——这个包到底交给谁?这就是传输层的第一件事:用端口给程序编号。这一章的主题:数据到了机器,交给哪个程序;以及,怎么把它可靠地传过去。
传输层(Transport Layer):在"机器到机器"之上解决"进程到进程"——用端口区分程序,用 TCP / UDP 决定怎么传。
1端口:数据交给哪个程序
端口(Port)是一个 16 位编号(0–65535)。IP 把包送到机器,端口把它送到机器里具体的某个程序或连接。一个 TCP 连接由四元组(four-tuple)唯一确定:源IP : 源端口 ↔ 目的IP : 目的端口。
0–1023 是知名端口(well-known):80 HTTP、443 HTTPS、22 SSH、53 DNS……0.1 里 curl 连的是 example.com 的 443 端口,那个 443 就是这里说的端口。
IP 像写字楼地址,端口像房间号:楼把信送到,房间号决定交给谁。对内核更精确的说法是——它拿四元组当 key,在连接表里查出该把数据交给哪个 socket,就是一次哈希查找。这个动作叫多路分解(demultiplexing)。
看本机在监听哪些端口(哪些程序在等连接),以及现有连接的四元组:
ss -tlnp # Linux:监听中的 TCP 端口(-t TCP -l 监听 -n 数字 -p 进程)
ss -tn # Linux:已建立的连接,每行就是一个四元组
lsof -iTCP -P -n # macOS
2TCP 三次握手
TCP(Transmission Control Protocol)是面向连接、可靠、有序的字节流。传数据前,双方先用三次握手建立连接——目的是同步彼此的初始序列号(ISN),并互相确认"我能发、你能收":
为什么是三次而不是两次?服务器发完 SYN+ACK 后,需要客户端的那个 ACK 来确认"客户端确实收得到我"。少了第三步,服务器无法确认反向通路通不通。这正是 0.1 里 curl 的 connect() 触发的过程。
关闭连接比建立多一步:四次挥手(FIN → ACK → FIN → ACK),因为两个方向要各自关闭(我说完了,但你可能还没说完)。细节这里不展开,记住"三次建、四次断"即可。
抓一次握手,亲眼看这三个包:
sudo tcpdump -i any -n 'tcp port 443' # macOS 换成 -i en0
curl https://example.com
你会看到 Flags [S](SYN)→ [S.](SYN-ACK)→ [.](ACK),正是三次握手;之后才轮到 TLS 握手和应用数据。回到 0.1 那个实验——现在这些 flag 你能读懂了。
还记得 0.3 那个 ttl=63 吗?TTL 在 IP 头里、每过一跳减 1,归零就被丢弃,用来防止包在环路里永远打转。traceroute 正是利用它:故意发 TTL=1、2、3… 的包,逼沿途每一跳依次"超时回报",从而一跳跳探出整条路径。跑 traceroute example.com(macOS 自带)或 tracepath example.com(Linux)试试。
3可靠传输:序号、ACK、重传
回忆 0.3:IP 是尽力而为(best-effort)——可能丢包、可能乱序、可能重复。那 TCP 的"可靠"从哪来?它在 IP 之上补了三件套:
- 序列号(sequence number):给字节流里每个字节编号,接收方据此重排乱序、丢弃重复;
- 确认(ACK):接收方回告"我已经收到到第几号了";
- 重传(retransmission):发送方在超时内没等到 ACK,就把那段重发一次。
这套"编号 + 确认 + 超时重发",和你在分布式系统里见到的 at-least-once 投递 + 按序号幂等去重是同一种思路。区别只是 TCP 把它做进了内核,对应用透明。
4流控与拥塞控制:别冲垮对方,也别压垮网络
能传了,还得控制速度。TCP 装了两个独立的"刹车":
- 流量控制(flow control):接收方用窗口大小(window size)告诉发送方"我缓冲区还能收多少",发送方据此限速。靠滑动窗口(sliding window)实现。它保护的是慢的接收方。
- 拥塞控制(congestion control):防止把网络本身塞爆。从小窗口慢启动(slow start)探起,一遇丢包就退让;常见算法有 CUBIC(Linux 默认)、BBR(Google)。它保护的是网络。
两个刹车同时生效,取更严格的那个决定实际发送速率。
接收方处理不过来,就缩小窗口让发送方慢下来——和你在 Kotlin Flow / 响应式流里见到的背压是同一个思想,只是 TCP 把它做在了传输层。下游顶不住,信号自动往上游传。
5UDP:要快不要可靠
UDP(User Datagram Protocol)走了相反的路:只在 IP 上加个端口就发,不连接、不确认、不排序、不限速。丢了就丢了,换来的是几乎为零的开销和最低的延迟。
| TCP | UDP | |
|---|---|---|
| 连接 | 面向连接(先握手) | 无连接,直接发 |
| 可靠性 | 可靠(ACK + 重传) | 尽力而为,可能丢 |
| 顺序 | 保证有序 | 不保证 |
| 开销 / 延迟 | 较高 | 极低 |
| 典型场景 | 网页、SSH、文件传输 | DNS、音视频、游戏、QUIC |
UDP 用在哪:DNS(0.5 马上就用)、实时音视频通话、在线游戏,以及 QUIC / HTTP3——后者干脆在 UDP 之上由应用自己重建可靠性,绕开内核 TCP 的历史包袱。
UDP 是"发完即忘(fire-and-forget)",TCP 是"带确认的可靠投递"。要省心就用 TCP;要自己精细掌控每个包(像 QUIC 那样)就用 UDP 自己搭一套。
你家路由器做端口转发(把外网某端口的流量转进内网某台机器),本质是 DNAT——按目的端口改写目的地址。它能把回包正确送回内网,靠的是 conntrack 记下的连接四元组。OpenWRT 的 firewall4 里,一条 port forward 规则必须指定 TCP 还是 UDP——正因为传输层有这两种独立协议;而且 TCP 有明确的握手 / 挥手状态可跟踪,UDP 没有连接状态,只能靠超时老化来"猜"连接结束。这条线 0.6 讲 NAT 和 conntrack 时会完整展开。
本章小结
- 端口(16 位)区分机器上的程序;一个 TCP 连接由四元组唯一确定,内核靠它把包分给正确的 socket(多路分解)。
- TCP:面向连接(三次握手)、可靠(序号 + ACK + 重传)、有序;在尽力而为的 IP 上补出可靠性。
- 流量控制(窗口)保护接收方,拥塞控制保护网络,共同限速;流控 = 传输层的背压。
- UDP:无连接、不可靠、低延迟;DNS、音视频、QUIC 在用。
- 路由器的端口转发按"协议 + 端口"工作,回包靠 conntrack 的四元组——0.6 展开。
动手练习
- 跑
ss -tlnp(或lsof -iTCP -P -n),挑一个监听端口,说出它是哪个程序、为什么用这个端口。 - 抓一次 TCP 握手:
sudo tcpdump -n 'tcp port 443',然后 curl 一个 https 站点,把[S]/[S.]/[.]三个包找出来。 - 思考题:DNS 查询通常用 UDP(53 端口)。为什么 DNS 适合 UDP 而非 TCP?(提示:一次查询就一问一答、包很小,握手那三个来回的代价划不划算?)
- 进阶:跑
traceroute example.com(或tracepath),数一数到目标经过几跳;结合 0.3 的ttl=63,解释 traceroute 是怎么用 TTL 一跳跳探路的。